外部ネットワークアクセスで RDS PostgreSQL に IAM認証で接続してみた #SnowflakeDB
はじめに
2024年11月のリリースで、外部ネットワークアクセスを使用する Snowpark UDF やストアドプロシージャから AWS サービスに対する IAM 認証のサポートが一般提供となりました。
さらに、同年10月のリリースでは、Snowpark から外部ネットワークにアクセスする際に AWS PrivateLink を利用する機能も一般提供されています。
これらの機能を組み合わせることで、プライベートサブネット内に構築した RDS に対して、IAM ロールベースのデータベース認証を使用する接続が可能になります。
IAM データベース認証では一時的な認証情報を使用しますが、この認証情報を取得する API を IAM 認証を設定した Snowflake インスタンスから呼び出す場合も、一時的な認証情報が利用されるため、認証情報の管理がよりセキュアになります。
これらの機能を実際に試してみましたので、本記事でその内容をまとめてみます。
前提条件
本記事では、以下の環境で検証を行いました。
PrivateLink 経由で RDS PostgreSQL に IAM データベース認証を行い UDF から PostgreSQL のデータを参照することを目指します。
- Snowflake
- Business Ciritical
- PrivateLink の設定には Business Ciritical 以上のエディションが必要
- クラウドリージョン:AWS_AP_NORTHEAST_1
- Business Ciritical
- AWS
- Snowflake アカウントと同一のリージョンに VPC を作成
- Amazon RDS PostgreSQL:16.3-R2
- プライベートサブネットに構築
- インスタンスタイプ:db.t3.micro
- RDS の DB インスタンスで IAM データベース認証を有効化済み
下図の構成です。
また、前提として上記の構成は設定済みで、すでに RDS PostgreSQL に対してはデータベースユーザーのユーザー名・パスワード認証にょる接続は可能な状態であるとします。こちらの構成手順については、以下をご参照ください。
サンプルデータ
サンプルデータには PostgreSQL 側で作成済みの以下のデータベース・テーブルを使用します。
-- データベースを作成
CREATE DATABASE sample_db;
\c sample_db;
-- テーブル: customers
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
age INT
);
-- customers テーブルにサンプルデータを挿入
INSERT INTO customers (name, email, age) VALUES
('Alice Smith', 'alice@example.com', 30),
('Bob Johnson', 'bob@example.com', 25),
('Charlie Brown', 'charlie@example.com', 35);
AWS側の作業
手順は以下に記載があるので、こちらに沿って進めます。
AWS 認証トークンを使用するデータベースユーザーアカウントを作成
踏み台サーバから PostgreSQL に接続し、以下のコマンドで IAM データベース認証用のデータベースユーザーを作成します。
--専用のロールを作成
CREATE ROLE readonly_role;
GRANT CONNECT ON DATABASE sample_db TO readonly_role;
\c sample_db;
GRANT USAGE ON SCHEMA public TO readonly_role;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;
-- IAMデータベース認証用のユーザーを作成し権限を付与
CREATE USER iam_ro_user WITH LOGIN;
GRANT rds_iam TO iam_ro_user;
GRANT readonly_role TO iam_ro_user;
データベースユーザーを IAM ロールにマッピングする IAM ポリシーを追加
以下の内容で IAM ポリシーを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"rds-db:connect"
],
"Resource": [
"arn:aws:rds-db:ap-northeast-1:<account-id>:dbuser:<dbi-resource-id>/iam_ro_user"
]
}
]
}
Amazon RDS のアクセスを許可する IAM ロールを作成
以下の内容で IAM ロールを作成します。
- 信頼されたエンティティのタイプ:AWS アカウント
- オプション
- 外部 ID を要求する
アカウントIDには一時的に自身のアカウントを指定し、外部 ID にも適当な値を指定しておきます。これらの値は後述する手順で更新します。
この時点では以下のようになります。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": {
"AWS": "<自身のAWSアカウントID>"
},
"Condition": {
"StringEquals": {
"sts:ExternalId": "<任意の値>"
}
}
}
]
}
Snowflake側の作業
ネットワークルールの作成
はじめにMODE = EGRESS、TYPE = PRIVATE_HOST_PORTとするネットワークルールを作成します。これにより外部サービスへのプライベートエンドポイント経由でのアクセスの許可・制限を行います。
VALUE_LISTには、RDS のエンドポイントとポートを指定します。注意点として、ポートの指定まで含めるようにします。この指定がない場合、デフォルトで 443 となり、サービスや設定よっては接続できなくなってしまいます。
CREATE OR REPLACE NETWORK RULE aws_rds_postgres_network_rule
MODE = EGRESS
TYPE = PRIVATE_HOST_PORT
VALUE_LIST = ('database-1.xxxxx.ap-northeast-1.rds.amazonaws.com:5432');
API 認証用のセキュリティ統合を作成
IAM 認証を使用する外部認証用のセキュリティ統合を作成します。これにより、Snowflake アカウント側でセキュリティ統合が参照する IAM ユーザーが作成されます。AWS_ROLE_ARN
には上記の手順で作成した IAM ロールの Arn を指定します。デフォルトでは ACCOUNTADMIN のみ実行可能です。
CREATE OR REPLACE SECURITY INTEGRATION rds_security_integration
TYPE = API_AUTHENTICATION
AUTH_TYPE = AWS_IAM
ENABLED = TRUE
AWS_ROLE_ARN = 'arn:aws:iam::<account-id>:role/sf-udf-rds-auth-role';
統合オブジェクトを作成後、作成したオブジェクトに対して以下を実行します。
DESC SECURITY INTEGRATION rds_security_integration;
出力の内、以下の内容を控えておきます。
- API_AWS_IAM_USER_ARN
- 統合オブジェクトが参照する Snowflake アカウント側の IAM ユーザー
- API_AWS_EXTERNAL_ID
AWS側:IAM ロールの信頼関係を更新
IAMロールの信頼関係を以下のように更新します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "<API_AWS_IAM_USER_ARN>"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "<API_AWS_EXTERNAL_ID>"
}
}
}
]
}
シークレットの作成
IAM ロールに設定された権限を引き受け、一時的な認証情報を得るためのシークレットオブジェクトを作成します。
CREATE OR REPLACE SECRET aws_rds_access_token
TYPE = CLOUD_PROVIDER_TOKEN
API_AUTHENTICATION = rds_security_integration;
外部ネットワークアクセス統合の作成
さいごに外部アクセス統合として、これまでに作成したネットワークルール・シークレットを紐づけます。デフォルトでは ACCOUNTADMIN のみ実行可能です。
--外部ネットワークアクセスを作成
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION rds_external_access_integration
ALLOWED_NETWORK_RULES = (<db>.<schema>.aws_rds_postgres_network_rule)
ALLOWED_AUTHENTICATION_SECRETS=(<db>.<schema>.aws_rds_access_token)
ENABLED=true ;
PythonUDF の作成
Snowflake UDF からの IAM データベース認証の使用は、以下のステップを行います。
- IAM ロールを引き受ける一時的な認証情報を取得(Snowflake 側 IAM ユーザーがロールを引き受け)
- 取得した一時的な認証情報で RDS が発行する認証トークンを取得
- 取得した認証トークンでデータベースに接続
IAM ロールを引き受ける一時的な認証情報を取得
RDS に接続する前に Snowpark UDF で一時的な認証情報を取得する手順を確認します。ここでは、以下のような Python UDF を作成し、実行してみます。
CREATE OR REPLACE FUNCTION fetch_temp_credentials()
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
HANDLER = 'get_secret_username_password'
EXTERNAL_ACCESS_INTEGRATIONS = (rds_iam_external_access_integration)
SECRETS = ('cred' = <db>.<schema>.aws_rds_access_token)
AS
$$
import _snowflake
import json
def get_secret_username_password():
cloud_provider_object = _snowflake.get_cloud_provider_token('cred')
cloud_provider_dictionary = {
"ACCESS_KEY_ID": cloud_provider_object.access_key_id,
"SECRET_ACCESS_KEY": cloud_provider_object.secret_access_key,
"TOKEN": cloud_provider_object.token
}
return json.dumps(cloud_provider_dictionary)
$$;
シークレットの情報から取得された IAM ロール引き比べのための一時的な認証情報の取得には_snowflake
モジュールを使用できます。
get_cloud_provider_token(cloud_provider_secret_name)
でセッションを作成するための情報を持つオブジェクトが返される- 属性として、
access_key_id
、secret_access_key
、token
が含まれる
この場合、出力は以下のようになります。
{
"ACCESS_KEY_ID": "xxxxxxxxxx",
"SECRET_ACCESS_KEY": "xxxxx",
"TOKEN": "xxxxxxxxxxxxxxxxxx"
}
RDS に IAM データベース認証で接続
RDS への IAM データベース認証を行うためには、認証トークンを取得する必要があります。このトークンは、上記の手順で取得した一時的な認証情報を使用したセッション内で発行されます。
この工程も含めた UDF として、ここでは以下の内容としました。
CREATE OR REPLACE function query_postgres()
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
IMPORTS=('@my_int_stage/ap-northeast-1-bundle.pem')
PACKAGES = ('psycopg2','boto3')
HANDLER = 'main'
EXTERNAL_ACCESS_INTEGRATIONS = (rds_iam_external_access_integration)
SECRETS = ('cred' = <db>.<schema>.aws_rds_access_token)
AS $$
import psycopg2
import sys
import os
import json
import boto3
import _snowflake
# RDS PostgreSQL の情報を設定
ENDPOINT = "database-1.xxxxx.ap-northeast-1.rds.amazonaws.com"
DBNAME = "sample_db"
USER = "iam_ro_user"
PORT = 5432
REGION = "ap-northeast-1"
# Snowflake UDFのインポートディレクトリから証明書ファイルのパスを取得
IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
ssl_cert_path = os.path.join(import_dir, 'ap-northeast-1-bundle.pem') # 証明書ファイルのパス
def main():
# シークレットから一時的な認証情報を取得
cloud_provider_object = _snowflake.get_cloud_provider_token('cred')
cloud_provider_dictionary = {
"ACCESS_KEY_ID": cloud_provider_object.access_key_id,
"SECRET_ACCESS_KEY": cloud_provider_object.secret_access_key,
"TOKEN": cloud_provider_object.token
}
# 認証情報の割り当てとリージョンを指定
boto3_session_args = {
'aws_access_key_id': cloud_provider_dictionary["ACCESS_KEY_ID"],
'aws_secret_access_key': cloud_provider_dictionary["SECRET_ACCESS_KEY"],
'aws_session_token': cloud_provider_dictionary["TOKEN"],
'region_name': 'ap-northeast-1'
}
# セッションを作成
session = boto3.Session(**boto3_session_args)
# RDS を操作するためのクライアントを作成
client = session.client('rds')
# RDS が発行する認証トークンを取得
token = client.generate_db_auth_token(DBHostname=ENDPOINT, Port=PORT, DBUsername=USER, Region=REGION)
# PostgreSQLに接続
conn = psycopg2.connect(
host=ENDPOINT,
port=PORT,
database=DBNAME,
user=USER,
password=token,
sslmode='verify-ca',
sslrootcert=ssl_cert_path # インポートディレクトリから取得した証明書ファイルを指定
)
# クエリを実行
cur = conn.cursor()
cur.execute("SELECT * FROM customers")
result = cur.fetchall()
# 接続を閉じる
conn.close()
return result
$$;
こちらは以下を参考とさせていただきました。
UDF を実行すると下図のようになり、PostgreSQL のデータを参照できました。
最後に、簡単ではありますが実行する SQL を変えて INSERT を実行する UDF としてみます。
insert 用の UDF
CREATE OR REPLACE function insert_postgres()
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
IMPORTS=('@my_int_stage/ap-northeast-1-bundle.pem')
PACKAGES = ('psycopg2','boto3')
HANDLER = 'main'
EXTERNAL_ACCESS_INTEGRATIONS = (rds_iam_external_access_integration)
SECRETS = ('cred' = <db>.<schema>.aws_rds_access_token)
AS $$
import psycopg2
import sys
import os
import json
import boto3
import _snowflake
# RDS PostgreSQL の情報を設定
ENDPOINT = "database-1.xxxxx.ap-northeast-1.rds.amazonaws.com"
DBNAME = "sample_db"
USER = "iam_ro_user"
PORT = 5432
REGION = "ap-northeast-1"
# Snowflake UDFのインポートディレクトリから証明書ファイルのパスを取得
IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
ssl_cert_path = os.path.join(import_dir, 'ap-northeast-1-bundle.pem') # 証明書ファイルのパス
def main():
# シークレットから一時的な認証情報を取得
cloud_provider_object = _snowflake.get_cloud_provider_token('cred')
cloud_provider_dictionary = {
"ACCESS_KEY_ID": cloud_provider_object.access_key_id,
"SECRET_ACCESS_KEY": cloud_provider_object.secret_access_key,
"TOKEN": cloud_provider_object.token
}
# 認証情報の割り当てとリージョンを指定
boto3_session_args = {
'aws_access_key_id': cloud_provider_dictionary["ACCESS_KEY_ID"],
'aws_secret_access_key': cloud_provider_dictionary["SECRET_ACCESS_KEY"],
'aws_session_token': cloud_provider_dictionary["TOKEN"],
'region_name': 'ap-northeast-1'
}
# セッションを作成
session = boto3.Session(**boto3_session_args)
# RDS を操作するためのクライアントを作成
client = session.client('rds')
# RDS が発行する認証トークンを取得
token = client.generate_db_auth_token(DBHostname=ENDPOINT, Port=PORT, DBUsername=USER, Region=REGION)
# PostgreSQLに接続
conn = psycopg2.connect(
host=ENDPOINT,
port=PORT,
database=DBNAME,
user=USER,
password=token,
sslmode='verify-ca',
sslrootcert=ssl_cert_path # インポートディレクトリから取得した証明書ファイルを指定
)
# クエリを実行
cur = conn.cursor()
cur.execute(
"INSERT INTO customers (name, email, age) VALUES (%s, %s, %s)",
('Alice Johnson', 'alice.johnson@example.com', 28)
)
result = cur.fetchall()
# 接続を閉じる
conn.close()
return result
$$;
IAM データベース認証用のユーザーは、読み取り専用権限しか与えていないので、書き込みを処理を行う UDF を実行すると下図のエラーとなります。
さいごに
外部ネットワークアクセスを使用する Snowpark UDF から RDS に IAM データベース認証で接続してみました。
IAM データベース認証では一時的な認証情報(トークン)を使用しますし、Snowflake との連携時も IAM 認証を使用することで、Snowflake 側でもトークン呼び出しに一時的な認証情報が利用されるため、よりセキュアになります。
こちらの内容が何かの参考になれば幸いです。